package lab.poc.erl;

import java.util.List;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.TimeoutException;

public abstract class Actor {

	static final ThreadLocal<Mailbox> MBOX = new ThreadLocal<Mailbox>();

	private Pid pid;
	private Node node;
	private Thread thread;
	private final Mailbox mailbox;

	private final ConcurrentSkipListSet<Pid> linkSet = new ConcurrentSkipListSet<Pid>();
	private boolean trapExitSignals = false;
	private volatile boolean isShutdown = false;

	public Actor() {
		this.mailbox = new Mailbox(this);
	}

	final void setPid(final Pid pid) {
		this.pid = pid;
	}

	final void setNode(final Node node) {
		this.node = node;
	}

	final void setThread(final Thread thread) {
		this.thread = thread;
	}

	final void lockMailbox() {
		mailbox.close();
	}

	/**
	 * Clears the mailbox.
	 */
	protected final void flushMailbox() {
		mailbox.flush();
	}

	protected final Node getNode() {
		return node;
	}

	protected final void trapExit(boolean trapExitSignals) {
		this.trapExitSignals = trapExitSignals;
	}

	protected final void exit() {
		if (thread != null) {
			thread.interrupt();
		}
	}

	protected final Pid self() {
		return pid;
	}

	/**
	 * Return the Pid of the parent process. Note that this may be {@code null} if
	 * the parent process ceased to exist or may refer to a "zombie" process if the
	 * parent process is currently executing its shutdown sequence but the parent pid
	 * hasn't been nulled out yet. 
	 * @return
	 */
	protected final Pid parent() {
		return pid.parent();
	}

	final void send(final Object message) {
		if (message != null) {
			mailbox.append(message);
		}
	}

	protected final Match receive(final Pattern... preds) {
		return mailbox.take(preds);
	}

	protected final Match receiveWithTimeout(long timeout,
			final Pattern... preds) throws TimeoutException {
		return mailbox.takeWithTimeout(timeout, preds);
	}

	protected final <R> MatchOrAfter<R> receiveWithTimeout(long timeout,
			final After<R> afterExpr, final Pattern... preds) {
		try {
			return new MatchOrAfter<R>(receiveWithTimeout(timeout, preds));
		} catch (TimeoutException e) {
			return new MatchOrAfter<R>(afterExpr.call());
		}
	}

	final Mailbox getMailbox() {
		return mailbox;
	}

	public static Match accept(final Pattern... preds) {
		final int MAX_REPEATS = 60;
		int retriesAttempted = 0;

		Mailbox mbox = null;
		while (mbox == null && ++retriesAttempted <= MAX_REPEATS) {
			int busySpin = 0;
			while (mbox == null && ++busySpin <= 4000) {
				mbox = MBOX.get();
				if (mbox != null) {
					return mbox.take(preds);
				}
			}
			try {
				Thread.sleep(16L);
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
			}
		}
		throw new IllegalThreadStateException(
				"non-existent thread-local Mailbox!");
	}

	public static Match acceptWithTimeout(long timeout, final Pattern... preds)
			throws TimeoutException {
		final int MAX_REPEATS = 60;
		int retriesAttempted = 0;

		Mailbox mbox = null;
		long start = System.currentTimeMillis();
		while (mbox == null && ++retriesAttempted <= MAX_REPEATS) {
			int busySpin = 0;
			while (mbox == null && ++busySpin <= 4000) {
				mbox = MBOX.get();
				if (mbox != null) {
					return mbox.takeWithTimeout(
							timeout - (System.currentTimeMillis() - start),
							preds);
				}
			}
			try {
				Thread.sleep(16L);
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
			}
		}
		throw new IllegalThreadStateException(
				"non-existent thread-local Mailbox!");
	}

	final boolean addToLinkSet(final Pid pid) {
		if (!isShutdown && !this.pid.equals(pid)) {
			return linkSet.add(pid);
		}
		return false;
	}

	final boolean removeFromLinkSet(final Pid pid) {
		return linkSet.remove(pid);
	}

	final void shutdown() {
		isShutdown = true;
		getNode().deregister(pid);
		pid.clearParentProcess();
		linkSet.clear();
	}

	final void notifyLinkedProcesses(final Exit exitType, final Throwable t) {
		for (final Pid pidLink : linkSet) {
			if (!trapExitSignals) {
				pidLink.signalExit(pid);
			} else {
				pidLink.unlink(pid);
				pidLink.send(new TrapExitMessage(exitType, pid, t));
			}
		}
	}

	protected abstract void onSpawn(final List<Object> args);
}
